package trace

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"strings"
	"sync"
	"sync/atomic"
	"time"
)

const (
	defaultSamplingRefreshInterval = time.Minute

	defaultSamplingUrl = "http://%s:5778/sampling"

	defaultProbability = 0.001
)

type samplingStrategyResponse struct {
	StrategyType          string
	ProbabilisticSampling probabilisticSamplingStrategy
	RateLimitingSampling  rateLimitingSamplingStrategy
	OperationSampling     perOperationSamplingStrategies
}
type probabilisticSamplingStrategy struct {
	SamplingRate float64
}

type rateLimitingSamplingStrategy struct {
	MaxTracesPerSecond int16
}

type perOperationSamplingStrategies struct {
	DefaultSamplingProbability       float64
	DefaultLowerBoundTracesPerSecond float64
	PerOperationStrategies           []*operationSamplingStrategy
	DefaultUpperBoundTracesPerSecond *float64
}

type operationSamplingStrategy struct {
	Operation             string
	ProbabilisticSampling *probabilisticSamplingStrategy
}

type remoteSampler struct {
	serviceName             string
	samplingRefreshInterval time.Duration
	storedStrategies        atomic.Value
	doneChan                chan *sync.WaitGroup
	defaultSampler          sampler
}

type storedStrategies struct {
	defaultStrategy   SamplerConfig
	serviceStrategies map[string]float64
}

func newRemoteSampler(serviceName string, samplingRefreshInterval time.Duration) sampler {
	sampler := &remoteSampler{
		serviceName:             serviceName,
		samplingRefreshInterval: samplingRefreshInterval,
		doneChan:                make(chan *sync.WaitGroup),
		defaultSampler:          newProbabilisticSampler(defaultProbability),
	}
	if sampler.samplingRefreshInterval == 0 {
		sampler.samplingRefreshInterval = defaultSamplingRefreshInterval
	}

	strategy, err := remoteStrategies(serviceName)
	if err != nil {
		_sdkLogger.Printf("failed to create remote sampler, use default: %+v", err)
		// if init from remote failed, use default remote strategy
		strategy = sampler.defaultRemoteStrategy()
	}

	sampler.parseStrategies(strategy)
	go sampler.pollController()
	return sampler
}

func (p *remoteSampler) defaultRemoteStrategy() samplingStrategyResponse {
	return samplingStrategyResponse{
		StrategyType: SamplerTypeProbabilistic,
		ProbabilisticSampling: probabilisticSamplingStrategy{
			SamplingRate: defaultProbability,
		},
		RateLimitingSampling: rateLimitingSamplingStrategy{},
		OperationSampling: perOperationSamplingStrategies{
			DefaultSamplingProbability: defaultProbability,
			PerOperationStrategies: []*operationSamplingStrategy{
				{
					Operation: "/health",
					ProbabilisticSampling: &probabilisticSamplingStrategy{
						SamplingRate: 0,
					},
				},
				{
					Operation: "/metrics",
					ProbabilisticSampling: &probabilisticSamplingStrategy{
						SamplingRate: 0,
					},
				},
				{
					Operation: "/monitor/ping",
					ProbabilisticSampling: &probabilisticSamplingStrategy{
						SamplingRate: 0,
					},
				},
			},
		},
	}
}

func (p *remoteSampler) loadStrategy(operationName string) (strategy SamplerConfig) {
	ss := p.storedStrategies.Load().(*storedStrategies)
	if _, ok := ss.serviceStrategies[operationName]; ok {
		strategy.Type = SamplerTypeProbabilistic
		strategy.Param = ss.serviceStrategies[operationName]
	} else {
		strategy = ss.defaultStrategy
	}
	return
}

func (p *remoteSampler) IsSampled(traceID uint64, operationName string) (bool, float64) {
	strategy := p.loadStrategy(operationName)
	if strategy.Type == SamplerTypeProbabilistic {
		if strategy.Param == defaultProbability {
			return p.defaultSampler.IsSampled(traceID, operationName)
		}
		return random(strategy.Param, operationName)
	}
	return false, 0
}

func (p *remoteSampler) Close() error {
	var wg sync.WaitGroup
	wg.Add(1)
	p.doneChan <- &wg
	wg.Wait()
	return nil
}

func (p *remoteSampler) pollController() {
	lastValue := ""
	ticker := time.NewTicker(p.samplingRefreshInterval)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			lastValue = p.reloadSamplingStrategy(lastValue)
		case wg := <-p.doneChan:
			wg.Done()
			return
		}
	}
}

func (p *remoteSampler) reloadSamplingStrategy(lastValue string) string {
	currBytes, err := fetch(p.serviceName)
	if err != nil {
		_sdkLogger.Printf("failed to load sampling from agent: %+v", err)
		return lastValue
	}
	newValue := string(currBytes)
	if lastValue == newValue {
		return lastValue
	}
	var strategy samplingStrategyResponse
	if err = json.Unmarshal(currBytes, &strategy); err != nil {
		_sdkLogger.Printf("failed to json unmarshal strategy response: %+v", err)
		return lastValue
	}
	p.parseStrategies(strategy)
	return newValue
}

func (p *remoteSampler) parseStrategies(strategy samplingStrategyResponse) {
	allper := strategy.OperationSampling.PerOperationStrategies
	operationStrategys := make(map[string]float64)

	for _, v := range allper {
		operationStrategys[v.Operation] = v.ProbabilisticSampling.SamplingRate
	}
	defaultStrategy := SamplerConfig{
		Type: strings.ToLower(strategy.StrategyType),
	}
	if defaultStrategy.Type == SamplerTypeProbabilistic {
		defaultStrategy.Param = strategy.ProbabilisticSampling.SamplingRate
	}
	if defaultStrategy.Type == SamplerTypeRateLimiting {
		defaultStrategy.Param = float64(strategy.RateLimitingSampling.MaxTracesPerSecond)
	}

	p.storedStrategies.Store(&storedStrategies{
		defaultStrategy:   defaultStrategy,
		serviceStrategies: operationStrategys,
	})
}

func remoteStrategies(serviceName string) (strategy samplingStrategyResponse, err error) {
	resp, err := fetch(serviceName)
	if err != nil {
		return samplingStrategyResponse{}, err
	}
	if err = json.Unmarshal(resp, &strategy); err != nil {
		return samplingStrategyResponse{}, err
	}
	return
}

func fetch(serviceName string) ([]byte, error) {
	v := url.Values{}
	v.Set("service", serviceName)

	uri := fmt.Sprintf(defaultSamplingUrl, os.Getenv("HOST")) + "?" + v.Encode()

	// TODO create and reuse http.Client with proper timeout settings, etc.
	resp, err := http.Get(uri)
	if err != nil {
		return nil, err
	}
	defer func() {
		if err := resp.Body.Close(); err != nil {
			_sdkLogger.Printf("failed to close HTTP response body: %+v", err)
		}
	}()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode >= 400 {
		return nil, fmt.Errorf("StatusCode: %d, Body: %s", resp.StatusCode, body)
	}
	return body, nil
}
